package model.data.tableModel;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.controlsfx.dialog.Dialogs;

import model.Configuration;
import model.handler.databaseHandling.QueryFactory;
import model.data.tableModel.person.Customer;
import model.data.tableModel.person.Staff;
import model.data.tableModel.sale.Product;
import model.data.tableModel.sale.Purchase;
import model.data.tableModel.sale.PurchaseContainsProduct;
import model.data.tableModel.sale.Salesday;
import model.data.wrapper.InputWrapper;
import model.data.wrapper.ToPay;
import model.dataStructures.DualMap;
import model.dataStructures.IDualMap;
import enums.PaymentType;
import enums.Property;
import enums.Status;
import view.GUI.mainView.KindVBox.BoxType;

/**
 * Controls all initialized data of the database.
 * <p>
 * Implemented as Singleton.
 * Contains maps containing all needed initialized data.
 * 
 * @version %I%, %G%
 */
public final class TableControl {

	private static Logger debug = LogManager.getLogger("DEBUG");

	/**
	 * Central counter for creating IDs for the different rows in every table.
	 */
	private static int centralCounter;
	
	/**
	 * Counter for billIDs
	 */
	private static int billCounter;
	
	/**
	 * Counter for purchase
	 */
	private static int purchaseCounter;

	/**
	 * Map containing all intitialized Staff
	 */
	private final IDualMap<Integer, String, Staff> staffMap;
	
	/**
	 * Map containing all intitialized Products
	 */
	private final IDualMap<Integer, String, Product> productMap;
	
	/**
	 * Map containing all intitialized Customer
	 */
	private final IDualMap<Integer, String, Customer> customerMap;

	private Salesday actualSalesday;
	private static TableControl instance;

	/**
	 * Private contructor. -> Implemented as Singleton
	 */
	private TableControl() {

		staffMap = new DualMap<>();
		productMap = new DualMap<>();
		customerMap = new DualMap<>();
	}

	/**
	 * @return Only instance of this class
	 */
	public static TableControl getInstance() {

		if (instance == null) {
			instance = new TableControl();
		}
		return instance;
	}

	/**
	 * Writes everything to the database. Is invoked if application is shut
	 * down.
	 */
	public void writeToDatabase() {

		actualSalesday.writeUpdateToDB();
		for (Customer c : customerMap.values()) {
			c.writeUpdateToDB();
		}
		QueryFactory.writeToProperty(Property.COUNTER, String.valueOf(centralCounter));
		QueryFactory.writeToProperty(Property.BILLCOUNTER, String.valueOf(billCounter));
		QueryFactory.writeToProperty(Property.PURCHASECOUNTER, String.valueOf(purchaseCounter));
		
		debug.debug("Final saving of TableControl before shutting down... Done.");
	}

	/**
	 * Computes the values of the inserted input.
	 * <p>
	 * The values computed in this method are displayed at the view.
	 * 
	 * @param inputMap Map containing all entered input
	 * @return ToPay wrapper containing all necessary values
	 */
	public ToPay computeInputValue(Map<BoxType, List<InputWrapper>> inputMap) {
		
		// Get lists of input of the different types
		List<InputWrapper> depositInput = inputMap.get(BoxType.DEPOSIT);
		List<InputWrapper> drinkInput = inputMap.get(BoxType.DRINKS);
		List<InputWrapper> groceryInput = inputMap.get(BoxType.GROCERIES);
		List<InputWrapper> otherInput = inputMap.get(BoxType.OTHER);		

		// Create wrapper containing all values
		ToPay toPay = new ToPay(-computeValueOfDepositBack(depositInput), computeValueOfInput(drinkInput), computeValueOfInput(groceryInput), computeValueOfInput(otherInput), computeDepositValue(drinkInput), inputMap);

		debug.debug("Input value computed");
		return toPay;
	}

	/**
	 * Writes the entered goods to the database.
	 * <p>
	 * Creates a new Bill and the BillContainsProduct objects and writes them to
	 * the database.
	 * 
	 * @param toPay Wrapper containing all information needed to write to the
	 *            database.
	 */
	public void bookPurchase(ToPay toPay) {
		debug.debug("Begin booking purchase with ToPay: "+ toPay);
		
		boolean isEmpty = toPay.getDepositBackValue() == 0 && toPay.getDepositValue() == 0 && toPay.getDrinkValue() == 0 && toPay.getGroceryValue() == 0 && toPay.getOtherValue() == 0;
		// if input is not empty
		if(!isEmpty){
			Purchase purchase = createPurchase(toPay);
			/*
			 *  List with all PurchaseContainsProduct instances which will be written
			 *  to the database if no error occurs (e.g. not enough stock left)
			 */			
			List<PurchaseContainsProduct> pcpList = new ArrayList<>();
			
			addPurchaseContainsProducts(toPay, purchase, pcpList);
			addDepositPurchaseContainsProduct(toPay, purchase, pcpList);
			
			purchase.writeToDB();
			for(PurchaseContainsProduct pcp: pcpList){
				pcp.writeToDB();
				pcp.getProd().writeUpdateToDB();
				if(pcp.getProd().getDeposit() != null){
					pcp.getProd().getDeposit().writeUpdateToDB();
				}
			}

			// Show ID of the bill to get signatures
			if (toPay.getCustomer().getPayment().equals(PaymentType.INVOICE)) {
				Dialogs.create().message("ID the Einkaufs: " + purchase.getPurchaseID()).showInformation();
				debug.debug("ID the Einkaufs: " + purchase.getPurchaseID());
			}
		}
	}


	
	// HELPER METHODS BELOW

	
	/**
	 * Creates new PurchaseContainsProduct instances for every deposit of a product that is bought and adds the instance to the given list.
	 * 
	 * @param toPay Wrapper class containing all information needed for writing a purchase to the database
	 * @param purchase Instance of the Purchase when the Product was bought
	 * @param pcpList List containing all PurchaseContainsProduct instances that are written to the database by the current Purchase.
	 */
	private void addDepositPurchaseContainsProduct(ToPay toPay, Purchase purchase, List<PurchaseContainsProduct> pcpList) {
		List<InputWrapper> drinkInput = toPay.getInputMap().get(BoxType.DRINKS);
		
		for(InputWrapper i: drinkInput){
			if(i.getSelectedProduct().getDeposit() != null){
				Product depositProduct = i.getSelectedProduct().getDeposit();
				int quantity = i.getBottleCount() + i.getTrayCount() * depositProduct.getMultiplicator();
				
				PurchaseContainsProduct pcp = PurchaseContainsProduct.createNewPurchaseContainsProduct(purchase, depositProduct, quantity, Status.ACTIVE);
				pcpList.add(pcp);
				
//				// If trays are sold create a new PurchaseContainsProduct instance which represents the trays being sold
//				if(i.getTrayCount() != 0){
//					Product emptyTray = productMap.get(Configuration.EMPTYTRAY);
//					PurchaseContainsProduct pcpTray = PurchaseContainsProduct.createNewPurchaseContainsProduct(purchase, emptyTray, i.getTrayCount(), Status.ACTIVE);
//					pcpList.add(pcpTray);
//				}
			}
		}
	}
	
	/**
	 * Checks if there is enough of the product left. If there is not enough,
	 * this method checks for replacement products and creates the new
	 * purchaseContainsProduct entry for it.
	 * 
	 * @param pcpList List where all PurchaseContainsProduct instances of the current purchase are stored
	 * @param pcp PurchaseContainsProduct instance which should be checked if there is enough stock of the product left
	 * @param quantity How much of the product should be sold
	 */
	private void checkForAvailability(List<PurchaseContainsProduct> pcpList, PurchaseContainsProduct pcp, int quantity, int trayCount) {

		Product prod = pcp.getProd();
		// If the product is not of the kind deposit
		if (!pcp.getProd().getCategory().equals(BoxType.DEPOSIT)) {
			int diff = 0;
			int trayDiff = 0;
			if (prod.getStock() <= quantity) {
				diff = quantity - prod.getStock();
				pcp.setQuantity(prod.getStock());
				if(prod.getReplacement() != null){
					prod.setStatus(Status.INACTIVE);
					prod.getReplacement().setStatus(Status.ACTIVE);
				}
			}
			if(prod.getCategory().equals(BoxType.DRINKS)){
				if(prod.getTrayCount() <= trayCount){
					trayDiff = trayCount - prod.getTrayCount();
					prod.setTrayCount(0);
				}
				else {
					prod.setStatus(Status.ACTIVE);
					pcp.setQuantity(quantity);
					prod.setTrayCount(prod.getTrayCount() - trayCount);
				}				
			}

			prod.reduceStock(pcp.getQuantity());
			
			if(diff != 0 || trayDiff != 0){
				PurchaseContainsProduct pcpReplacement = PurchaseContainsProduct.createNewPurchaseContainsProduct(pcp.getPurchase(), prod.getReplacement(), diff, Status.ACTIVE);
				checkForAvailability(pcpList, pcpReplacement, diff, trayDiff);
				pcpList.add(pcpReplacement);
			} 

			// if deposit is brought back add the number of the deposit the stock
		} else if (prod.getCategory().equals(BoxType.DEPOSIT)) {
			if (quantity < 0) {
				prod.addStock(quantity * (-1));
			}
		}
	}

	/**
	 * Creates new PurchaseContainsProduct instances for every product that is bought and adds the instance to the given list.
	 * 
	 * @param toPay Wrapper class containing all information needed for writing a purchase to the database
	 * @param purchase Instance of the Purchase when the Product was bought
	 * @param pcpList List containing all PurchaseContainsProduct instances that are written to the database by the current Purchase.
	 */
	private void addPurchaseContainsProducts(ToPay toPay, Purchase purchase, List<PurchaseContainsProduct> pcpList) {
		Map<BoxType, List<InputWrapper>> inputMap = toPay.getInputMap();
		for(List<InputWrapper> list: inputMap.values()){
			for(InputWrapper i: list){
				Product product = i.getSelectedProduct();
				int multiplicator = product.getMultiplicator();
				int trayQuantity = i.getTrayCount();
				
				// If a Product has a deposit, the depositProduct contains the multiplicator information
				if(product.getDeposit() != null){multiplicator = product.getDeposit().getMultiplicator();}
				int quantity = multiplicator * i.getTrayCount() + i.getBottleCount();
				
				if(product.getAvailability() < quantity && !product.getCategory().equals(BoxType.DEPOSIT)){
					debug.debug("Es sind nicht mehr genügend Einheiten von Produkt " + product.getName() + " vorhanden! Buchung abgebrochen.");
					throw new IllegalArgumentException("Es sind nicht mehr genügend Einheiten von Produkt " + product.getName() + " vorhanden! Buchung abgebrochen.");
				}
				
				// If deposit is brought back the quantity is negative
				if(i.getSelectedProduct().getCategory().equals(BoxType.DEPOSIT)){quantity = quantity * -1; trayQuantity = trayQuantity * -1;}
				
				PurchaseContainsProduct pcp = PurchaseContainsProduct.createNewPurchaseContainsProduct(purchase, i.getSelectedProduct(), quantity, Status.ACTIVE);
				checkForAvailability(pcpList, pcp, quantity, i.getTrayCount());
				
				// If trays are brought back
				Product emptyTray = productMap.get(Configuration.EMPTYTRAY);
				if(product.getCategory().equals(BoxType.DRINKS) || product.getCategory().equals(BoxType.DEPOSIT)){
					if(i.getTrayCount() != 0 && !product.equals(emptyTray)){
						if(i.getSelectedProduct().getCategory().equals(BoxType.DEPOSIT)){
							emptyTray.addStock(Math.abs(trayQuantity));	// add quantity of trays brought back to the stock of the empty tray product	
						} 
						PurchaseContainsProduct pcpTray = PurchaseContainsProduct.createNewPurchaseContainsProduct(purchase, emptyTray, trayQuantity, Status.ACTIVE);
						pcpList.add(pcpTray);
					}					
				}
				pcpList.add(pcp);
			}
		}
	}

	/**
	 * Creates a Purchase instance for the purchase that should be written to the database.
	 * @param toPay Wrapper of the input to book
	 * @return Purchase instance
	 */
	private Purchase createPurchase(ToPay toPay) {
		// If the customer pays cash the purchase is already paid
		boolean isPaid = false;
		if(toPay.getCustomer().getPayment().equals(PaymentType.CASH)){ isPaid = true;}
		
		Purchase purchase = Purchase.createNewPurchase(toPay.getCustomer().getBillID(), isPaid, this.actualSalesday, Status.ACTIVE);
		return purchase;
	}
	
	/**
	 * Computes the value of the deposit of the given input.
	 * <p>
	 * Writes the value of the deposit to the inputWrapper and return the value of the whole deposit.
	 * @param input
	 * @return The value of all deposit that is bought
	 */
	private double computeDepositValue(List<InputWrapper> input) {
		double value = 0.0;
		
		for(InputWrapper i: input){
			Product depositProd = i.getSelectedProduct().getDeposit();
			double singleValue = 0.0;
			
			if(depositProd != null){
				int multiplicator = depositProd.getMultiplicator();
				singleValue += i.getTrayCount() * productMap.get(Configuration.EMPTYTRAY).getSalePrice() + (multiplicator * i.getTrayCount() + i.getBottleCount()) * depositProd.getSalePrice();				
			}
			
			i.setDepositCount(singleValue);
			value += singleValue;
		}
		return value;
	}

	/**
	 * Computes the value of a given input.
	 * <p>
	 * Writes the value of each product that will be bought to the InputWrapper and returns the value for the value of the whole input.
	 * @param input List containing all InputWrapper
	 * @return The whole value of the given input
	 */
	private double computeValueOfInput(List<InputWrapper> input) {
		double value = 0.0;
		for(InputWrapper i: input){
			
			Product prod = i.getSelectedProduct();
			int multiplicator = prod.getMultiplicator();
			if(prod.getDeposit() != null){multiplicator = prod.getDeposit().getMultiplicator();}
			double singleValue = (multiplicator * i.getTrayCount() + i.getBottleCount()) * prod.getSalePrice();
			i.setValue(singleValue);
			value += singleValue;
			System.out.println(multiplicator + " value: " + singleValue);
		}
		return value;
	}
	
	private double computeValueOfDepositBack(List<InputWrapper> input){
		double value = 0.0;
		for(InputWrapper i: input){
			
			Product prod = i.getSelectedProduct();
			int multiplicator = prod.getMultiplicator();
			double singleValue = (multiplicator * i.getTrayCount() + i.getBottleCount()) * prod.getSalePrice();
			if(!prod.getName().equals(Configuration.EMPTYTRAY.toString())){
				singleValue += i.getTrayCount() * productMap.get(Configuration.EMPTYTRAY.toString()).getSalePrice();
			}
			
			i.setValue(singleValue);
			value += singleValue;
		}
		return value;
	}

	
	/* GETTER AND SETTER BELOW */	
	
	/**
	 * Sets the global ID counter.
	 * Is invoked only once - when the application is launched.
	 * @param counter Value of the counter
	 */
	public static void setCentralCounter(int counter) {
		
		assert counter >= 0;
		
		TableControl.centralCounter = counter;
		debug.debug("Global counter set to: " + counter);
	}
	
	/**
	 * Sets the global purchase ID counter.
	 * Is invoked only once - when the application is launched.
	 * @param counter Value of the counter
	 */
	public static void setPurchaseCounter(int counter) {
		
		assert counter >= 0;
		
		TableControl.purchaseCounter = counter;
		debug.debug("Purchase counter set to: " + counter);
	}
	
	/**
	 * Sets the global bill ID counter.
	 * Is invoked only once - when the application is launched.
	 * @param counter Value of the counter
	 */
	public static void setBillCounter(int counter) {
		
		assert counter >= 0;
		
		TableControl.billCounter = counter;
		debug.debug("Bill counter set to: " + counter);
	}
	
	/**
	 * @return Map containing all initialized Staff
	 */
	public IDualMap<Integer, String, Staff> getStaffMap() {
		
		return staffMap;
	}
	
	/**
	 * @return The actual Salesday
	 */
	public Salesday getActualSalesday() {
		
		return actualSalesday;
	}
	
	/**
	 * Returns the global ID counter and increments it and writes the new value to the database.
	 * @return
	 */
	public static int getCentralCounter() {
		
		QueryFactory.writeToProperty(Property.COUNTER, String.valueOf(++centralCounter));
		debug.debug("Get counter: " + centralCounter);
		return centralCounter = centralCounter++;
	}
	
	/**
	 * Returns the global Bill ID counter and increments it and writes the new value to the database.
	 * @return
	 */
	public static int getBillCounter() {
		
		QueryFactory.writeToProperty(Property.BILLCOUNTER, String.valueOf(++billCounter));
		debug.debug("Get billCounter: " + billCounter);
		return billCounter = billCounter++;
	}
	/**
	 * Returns the global Purchase ID counter and increments it and writes the new value to the database.
	 * @return
	 */
	public static int getPurchaseCounter() {
		
		QueryFactory.writeToProperty(Property.PURCHASECOUNTER, String.valueOf(++purchaseCounter));
		debug.debug("Get purchaseCounter: " + purchaseCounter);
		return purchaseCounter = purchaseCounter++;
	}
	/**
	 * Sets the actual Salesday and writes it to the database.
	 * @param actualSalesday
	 */
	public void setActualSalesday(Salesday actualSalesday) {

		assert actualSalesday != null;
		this.actualSalesday = actualSalesday;
		
		debug.debug("Actual Salesday: " + actualSalesday);
		actualSalesday.writeToDB();
	}
	
	/**
	 * @return The map containing all customer that are initialized
	 */
	public IDualMap<Integer, String, Customer> getCustomerMap() {

		return customerMap;
	}

	/**
	 * @return The map containing all products that are initialized
	 */
	public IDualMap<Integer, String, Product> getProductMap() {

		return productMap;
	}

	/**
	 * @param productName Name of the product that should be returned
	 * @return The product with the given name
	 */
	public Product getProduct(String productName) {

		assert productName != null && !"".equals(productName);
		return productMap.get(productName);
	}
}
